基于有向距离场(SDF)的地图碰撞系统 | Cocos 技术派第15期
受访者:书生
编辑:C姐
传统方案上,对于这种场景的设计,大家首先想到的肯定是物理引擎,通过设置建筑物和障碍物的碰撞体(Collider)来阻挡人物的行动。
在这种思路下,如果场景中的建筑物和人物比较多,会造成比较严重的性能问题,因为每一帧内对每一个人物和每一个障碍物都需要做碰撞检测,计算量是:N (人物) * M (障碍物)。再加上飞镖的碰撞检测计算量,在不支持 JIT 的 iOS 平台上可能会有不小的性能压力。当然,基于物理引擎的碰撞检测方式也有不少可以优化的点,比如说:
使用简单的 Builtin Physics 替代 Cannon 物理后端
通过场景管理剔除不在可视范围内的物体的碰撞计算
简化 3D 碰撞检测为 2D 碰撞检测,简化盒子碰撞体为圆形碰撞体
但是这些优化的效率都远远不如《吃鸡联盟》中所应用的有向距离场碰撞系统。下面就来看看开发团队倾囊相授的基于 Cocos Creator 3D 如何实现这样一套场景碰撞检测系统吧!
原文作者:kx-dz
首先,大概实现的原理是通过插值计算得出任意点的有向距离数据,然后与单位的碰撞大小做比对,来检测单位是否可以通行。实例图如下:
1. 原理
通过有向距离数据,我们可以通过计算差值的方式算出任意点到最近障碍的距离。
2. 具体实现
既然这么棒,那么,要怎样获得这些数据呢?
2.1:栅格化地图数据
就是将地图划分为N*N个格子,每个格子标记为可通行/不可通行。当然,划分的格子越多,精度越高。
建议使用高度图来存储通行数据,高度图长这个样子:
2.2:读取栅格数据
准备好图片后就需要读取像素信息了。
(关于原生url的获取,暂时没太好的方法,只有先load资源然后再获取nativeUrl值。如果有更好的方法请告知)
//获取指定图片文件的像素数据。返回Promise
//path写到文件名就行,不需要加spriteFrame和后缀
loadImagePixelData(path:string){
var self = this
return new Promise((resolve,reject)=>{
loader.loadRes(path+"/spriteFrame",SpriteFrame,(err,res)=>{
if(err){
console.error(err)
return reject();
}
var spriteFrame = <SpriteFrame>res;
var rect = spriteFrame.rect;
var img = new Image();
img.src = spriteFrame.texture.image.nativeUrl;
// console.log(spriteFrame._image.nativeUrl);
// console.log(spriteFrame._image.url);
img.onload=()=>{
self.context.drawImage(img,0,0,rect.width,rect.height);
var imageData = self.context.getImageData(0,0,rect.width,rect.height);
resolve(imageData);
}
img.onerror=()=>{
reject("Error:load img failed!Path="+path);
}
})
});
}
{
data: [0,0,0,255,0,0,0,255,…],
height:128,
width:128
}
//高度图数据转化为地图通行数据
//imgData格式:{data:Uint8ClampedArray,width:number,height:number}
imgData2PassData(imgData:any){
var data = imgData.data;
var result = [];
var width = imgData.width;
var height = imgData.height;
if(data.length<width*height*4){
console.error("Error:图片数据长度不足!")
return [];
}
var count = 0;
for(var y=0;y<height;y++){
var arr = [];
for(var x=0;x<width;x++){
var r = data[count];
var g = data[count+1];
var b = data[count+2];
arr.push(r>128&&g>128&&b>128);
count+=4;
}
result.push(arr);
}
return result;
}
//存储通行数据,这一步上面做过了
private _blocks=[]
//用来存储有向距离数据
private _distances=[];
initSdfSys(){
var gridCountH = 128;
var gridCountV = t128;
this._distances=[];
for(let i=0;i<gridCountV+1;i++){
let dataArr = [];
for(let j=0;j<gridCountH+1;j++){
var value=0;
dataArr.push(value);
}
this._distances.push(dataArr);
}
this.refreshData();
}
private refreshData(){
for(let y=0;y<this._distances.length;y++){
for(let x=0;x<this._distances[y].length;x++){
this._distances[y][x] = this._checkDis(x,y);
}
}
}
//距离检测
private _checkDis(vertX:number,vertY:number):number{
var result;
for(let y=0;y<this._blocks.length;y++){
for(let x=0;x<this._blocks[y].length;x++){
if(this._blocks[y][x]){
let dis;
if(y>=vertY&&x>=vertX){
dis = Math.floor(this.gridSize*(Math.sqrt(Math.pow(y-vertY,2)+Math.pow(x-vertX,2))));
}
else if(y<vertY&&x>=vertX){
dis = Math.floor(this.gridSize*(Math.sqrt(Math.pow(y-vertY+1,2)+Math.pow(x-vertX,2))));
}
else if(y>=vertY&&x<vertX){
dis = Math.floor(this.gridSize*(Math.sqrt(Math.pow(y-vertY,2)+Math.pow(x-vertX+1,2))));
}
else if(y<vertY&&x<vertX){
dis = Math.floor(this.gridSize*(Math.sqrt(Math.pow(y-vertY+1,2)+Math.pow(x-vertX+1,2))));
}
if(isNaN(result)||dis<result) result=dis;
}
}
}
return result||0;
}
因为计算量高达 (N+1)(N+1)N*N 次,可能会消耗大量时间。经试验,一张网格尺寸为 128*128 的地图,在纯 H5 环境以及安卓的微信小游戏环境下,计算速度尚能接受,但是在 iOS 的微信小游戏环境下,计算时间高达 50s,这显然是不能接受的。
所以,推荐使用事先处理好数据,然后导出 json 文件的方式,游戏运行时直接读取现成的 json 文件即可。
这就是以内存空间换取速度的思想,也是 SDF 系统的核心思想。
calPointDis(pos:Vec3){
var gridLen = 32;
var gridPos = this.nodePos2GridPos(pos);
if(this._block[gridPos.y]&& this._block[gridPos.y][gridPos.x]) return 0;
var posZero = this.vertexPos2NodePos(gridPos.x,gridPos.y);
var parmX = (pos.x-posZero.x)/gridLen;
var parmY = (pos.z-posZero.z)/gridLen;
var dis_lt = this._distances[gridPos.y+1][gridPos.x];
var dis_ld = this._distances[gridPos.y][gridPos.x];
var dis_rt = this._distances[gridPos.y+1][gridPos.x+1];
var dis_rd = this._distances[gridPos.y][gridPos.x+1];
var dis = (1-parmX)*(1-parmY)*dis_ld+parmX*(1-parmY)*dis_rd+(1-parmX)*parmY*dis_lt+parmX*parmY*dis_rt;
return dis;
}
calGradient(pos:Vec3):Vec3{
var delta=0.1;
var dis0 = this.calPointDis(new Vec3(pos.x+delta,0,pos.z));
var dis1 = this.calPointDis(new Vec3(pos.x-delta,0,pos.z));
var dis2 = this.calPointDis(new Vec3(pos.x,0,pos.z+delta));
var dis3 = this.calPointDis(new Vec3(pos.x,0,pos.z-delta));
var result = new Vec3(dis0-dis1,0,dis2-dis3).multiplyScalar(0.5);
return result.normalize();
}
具体的处理碰撞的代码:
update (deltaTime: number) {
if(this._isControlledByJoystick&&this._speedRatio>0){
var curPos = this.node.position.clone();
var moveDis_dt = this.curSpeed*deltaTime;
var newPos = curPos.clone().add(this.dir.clone().multiplyScalar(moveDis_dt));
var sd = this.ground.calPointDis(newPos);
if(sd<this.collideRaduis){
//console.log("sd=",sd);
var gradient = this.ground.calGradient(newPos);
var adjustDir = this.dir.clone().subtract(gradient.clone().multiplyScalar(Vec3.dot(gradient,this.dir)))
//console.log(StringUtils.format("dir=%s,gradient=%s,adjustDir=%s",this.dir,gradient,adjustDir));
newPos = curPos.clone().add(adjustDir.normalize().multiplyScalar(moveDis_dt));
for(var i=0;i<3;i++){
sd = this.ground.calPointDis(newPos);
if(sd>=this.collideRaduis) break;
newPos.add(this.ground.calGradient(newPos.clone()).multiplyScalar(this.collideRaduis-sd));
}
// sd = this.ground.calPointDis(newPos);
// if(sd<this.collideRaduis){
// newPos.add(this.ground.calGradient(newPos.clone()).multiplyScalar(this.collideRaduis-sd));
// }
//避免往返
if(Vec3.dot(newPos.clone().subtract(curPos),this.dir.clone())<0){
newPos = curPos;
}
}
this.node.setPosition(newPos);
this.onMove();
}
}
关于 SDF 的细节就不在这里向大家详细解读了,目前这方面的资料相对比较稀少,感兴趣的开发者朋友,可以参考《腾讯游戏开发精粹》这本书进行学习,此外,也可以多逛逛 Cocos 官方论坛,论坛上还是有很多有价值的学习资料可以挖掘的。
最后,要感谢 Cocos 团队在这一年多来给予我们的支持。由于 Cocos Creator 引擎的易用性,在19年做小游戏时,我们就把 Cocos Creator 作为首选引擎,后期推出了 3D 引擎,我们几乎没有花学习成本就成功完成了从 2D 到 3D 的团队转型。
在最开始使用 3D 引擎时,我们对于优化毫无经验,Cocos 给我们提供了很多很好的思路。特别是到了后期,产品开始一些跨渠道跨平台的运营时,关于各个渠道的平台差异所产生的分包问题以及参数配置问题,这些对于小开发者很不友好,没有经验的情况下,可能要花很长时间来应对这些问题,幸好我们跟 Cocos 团队有密切的沟通,这些问题都很快得到解决。
以上就是本期技术派的全部内容啦!非常感谢制作人书生接受 Cocos 的专访,也非常感谢《吃鸡联盟》团队慷慨的技术分享,希望游戏可以取得好成绩!
点击【阅读原文】可进入社区原贴与作者交流,为作者点赞哟!也欢迎大家点击下方的小程序链接,体验这款小游戏。